package ezcaptcha

import (
	"context"
	"errors"
	"gitee.com/kaylee595/gkaylee/kerr"
	"github.com/imroc/req/v3"
	"time"
)

type Client[C TaskC, R TaskR] struct {
	HTTPClient *req.Client
	clientKey  string
	region     *Region
}

type Option[C TaskC, R TaskR] func(*Client[C, R])

func WithRegion[C TaskC, R TaskR](region *Region) Option[C, R] {
	return func(c *Client[C, R]) {
		if region == nil {
			region = RegionGlobal
		}
		c.region = region
	}
}

func NewClient[C TaskC, R TaskR](httpClient *req.Client, clientKey string, opts ...Option[C, R]) *Client[C, R] {
	if httpClient == nil {
		httpClient = req.DefaultClient()
	}
	c := &Client[C, R]{clientKey: clientKey, HTTPClient: httpClient, region: RegionGlobal}
	for _, opt := range opts {
		opt(c)
	}
	return c
}

type TaskId string

type respCreateTask struct {
	ErrorId          int    `json:"errorId"`
	ErrorCode        string `json:"errorCode"`
	ErrorDescription string `json:"errorDescription"`
	TaskId           TaskId `json:"taskId"`
}

func (r *respCreateTask) ToErr() error {
	return &ErrResp{
		ErrorId:          r.ErrorId,
		ErrorCode:        r.ErrorCode,
		ErrorDescription: r.ErrorDescription,
	}
}

func (c *Client[C, R]) CreateTask(ctx context.Context, i C) (TaskId, error) {
	resp := c.HTTPClient.Post(c.region.NewURLString("/createTask")).SetBodyJsonMarshal(map[string]any{
		"clientKey": c.clientKey,
		"task":      i,
	}).Do(ctx)
	if resp.Err != nil {
		return "", kerr.WrapTraceStack(resp.Err)
	}
	ret := &respCreateTask{}
	if err := UnmarshalJson(resp, &ret); err != nil {
		return "", kerr.WrapTraceStack(err)
	}
	if ret.ErrorId != 0 {
		return "", kerr.WrapTraceStack(ret.ToErr())
	}
	return ret.TaskId, nil
}

type TaskStatus string

const (
	TaskStatusProcessing TaskStatus = "processing"
	TaskStatusReady      TaskStatus = "ready"
)

type RespGetTaskResult[R TaskR] struct {
	Solution R          `json:"solution"`
	Status   TaskStatus `json:"status"`
}

type respGetTaskResult[R TaskR] struct {
	ErrorId          int        `json:"errorId"`
	ErrorCode        string     `json:"errorCode"`
	ErrorDescription string     `json:"errorDescription"`
	Solution         R          `json:"solution"`
	Status           TaskStatus `json:"status"`
}

func (r *respGetTaskResult[R]) ToErr() error {
	return &ErrResp{
		ErrorId:          r.ErrorId,
		ErrorCode:        r.ErrorCode,
		ErrorDescription: r.ErrorDescription,
	}
}

func (r *respGetTaskResult[R]) WithoutError() *RespGetTaskResult[R] {
	return &RespGetTaskResult[R]{
		Solution: r.Solution,
		Status:   r.Status,
	}
}

func (c *Client[C, R]) GetTaskResult(ctx context.Context, taskId TaskId) (*RespGetTaskResult[R], error) {
	resp := c.HTTPClient.Post(c.region.NewURLString("/getTaskResult")).SetBodyJsonMarshal(map[string]any{
		"clientKey": c.clientKey,
		"taskId":    taskId,
	}).Do(ctx)
	if resp.Err != nil {
		return nil, kerr.WrapTraceStack(resp.Err)
	}
	ret := &respGetTaskResult[R]{}
	if err := UnmarshalJson(resp, &ret); err != nil {
		return nil, kerr.WrapTraceStack(err)
	}
	if ret.ErrorId != 0 {
		return nil, kerr.WrapTraceStack(ret.ToErr())
	}
	return ret.WithoutError(), nil
}

func (c *Client[C, R]) WaitForResult(ctx context.Context, i C) (R, error) {
	taskId, err := c.CreateTask(ctx, i)
	if err != nil {
		return *new(R), kerr.WrapTraceStack(err)
	}
	for {
		select {
		case <-ctx.Done():
			return *new(R), kerr.WrapTraceStack(ctx.Err())
		case <-time.After(5 * time.Second):
		}
		result, err := c.GetTaskResult(ctx, taskId)
		if err != nil {
			return *new(R), kerr.WrapTraceStack(err)
		}
		if result.Status == TaskStatusReady {
			return result.Solution, nil
		}
	}
}

func (c *Client[C, R]) WaitForResult2m(ctx context.Context, i C) (R, error) {
	timeoutCtx, cancel := context.WithTimeoutCause(ctx, 2*time.Minute, ErrWaitForResultTimeout)
	defer cancel()
	result, err := c.WaitForResult(timeoutCtx, i)
	if err != nil && errors.Is(context.Cause(timeoutCtx), ErrWaitForResultTimeout) {
		err = ErrWaitForResultTimeout
	}
	return result, err
}
